Skip to content

feat(react-headless-components-preview): add focustrap#36123

Open
mainframev wants to merge 1 commit into
microsoft:masterfrom
mainframev:feat/headless-popover-focustrap
Open

feat(react-headless-components-preview): add focustrap#36123
mainframev wants to merge 1 commit into
microsoft:masterfrom
mainframev:feat/headless-popover-focustrap

Conversation

@mainframev
Copy link
Copy Markdown
Contributor

@mainframev mainframev commented May 8, 2026

Previous Behavior

Popover had no focus trap. The surface rendered as a <div popover="auto"> and relied entirely on the browser's light-dismiss (Escape, click-outside, popover-stack peer dismissal).

New Behavior

Adds a trapFocus prop on Popover that delegates modal behavior. The surface always renders as a single element; the show mode is wired in usePopover:

  • trapFocus={false} (default) — surface.showPopover(). Browser owns light dismiss; no focus trap or autofocus. Explicit role="group" overrides the dialog's implicit role="dialog" so assistive tech
    doesn't announce modal semantics that aren't present.
  • trapFocus={true} — surface.showModal(). The platform supplies the focus trap, autofocus, trigger-restoration on close, and inert background — all spec-mandated by . The explicit role is
    dropped so the implicit role="dialog" + aria-modal="true" apply. The cancel event is intercepted (preventDefault + close via onOpenChange) so Escape flows through the same React state path as any other
    dismiss.

A single element supporting both modes means consumers flip one prop instead of swapping component variants, and there's no portal/stacking-context divergence between the two paths.

IMPORTANT NOTE:

There one notable behavioral difference compared to a custom focus trap implementation: By default with a native dialog, all elements within the same document, except the dialog and its descendants become inert, but it is scoped only to the containing document. If the dialog is rendered inside an iframe, the rest of the parent page remains interactive (focusable), which differs to v9 behaviour

Example: https://dialog-focus-trap-test.surge.sh/

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 8, 2026

📊 Bundle size report

Package & Exports Baseline (minified/GZIP) PR Change
react-headless-components-preview
react-headless-components-preview: entire library
107.577 kB
31.646 kB
107.958 kB
31.776 kB
381 B
130 B

🤖 This report was generated against 39041d7c84a1e8cca8b74703ddef48b1a88e1a0c

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 8, 2026

Pull request demo site: URL

@mainframev mainframev force-pushed the feat/headless-popover-focustrap branch from b78749b to 9a7ebfb Compare May 8, 2026 16:00
@mainframev mainframev force-pushed the feat/headless-popover-focustrap branch from 9a7ebfb to d0ef8d5 Compare May 8, 2026 22:45
@mainframev mainframev marked this pull request as ready for review May 11, 2026 01:22
@mainframev mainframev requested a review from a team as a code owner May 11, 2026 01:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant